package
{
	import flash.display.Sprite;
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.external.ExternalInterface;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestHeader;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	import flash.system.Security;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	
	/**
	 * Flash-based proxy for cross-domain requests
	 * @author aflesler
	 */ 
	[SWF(width=1, height=1, frameRate=5)]
	public class cajax extends Sprite
	{
		private static const EMPTY_BODY_FIX:String = ' ';
		
		private var loaders:Dictionary;
		
		public function cajax()
		{
			init();
		}
		
		private function init():void
		{
			loaders = new Dictionary();
			Security.allowDomain('*');
			Security.allowInsecureDomain('*');
			
			ExternalInterface.addCallback('request', request);
			ExternalInterface.addCallback('abort', abort);
			call('dequeue');
		}
		
		// Exposed methods
		
		private function request(id:int, url:String, method:String=URLRequestMethod.GET, data:Object=null, headers:Object=null):void
		{
			method = method.toUpperCase();
			
			var request:URLRequest = new URLRequest(url);
			request.data = setupData(data, method);
			request.requestHeaders = setupHeaders(headers);
			request.method = method;
			
			var loader:URLLoader = new URLLoader(request);
			loader.addEventListener(Event.COMPLETE, handleComplete);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleError);
			loader.addEventListener(IOErrorEvent.IO_ERROR, handleError);
			
			loaders[loader] = id;
		}
		
		private function abort(id:int):void
		{
			for (var loader:* in loaders) {
				if (loaders[loader] === id) {
					URLLoader(loader).close();
					cleanLoader(loader);
					break;
				}
			}
		}
		
		// Request Utils
		
		private function setupData(data:Object, method:String):Object
		{
			// TODO: How to implement? JS cannot pass a byteArray
			if (data is ByteArray) {
				return data;
			}
			
			// @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/URLRequest.html#method
			// Flash automatically converts POST to GET when the body is empty			
			if (method === URLRequestMethod.POST && !data) {
				return EMPTY_BODY_FIX;
			}
			
			if (!data || data is String && method === URLRequestMethod.POST) {
				return data;
			}
			
			if (data is String) {
				return new URLVariables(String(data));
			}
			
			var vars:URLVariables = new URLVariables();
			for (var key:String in data) {
				vars[key] = data[key];
			}
			return vars;
		}
		
		private function setupHeaders(headers:Object):Array
		{
			var ret:Array = [];
			for (var key:String in headers) {
				ret.push(new URLRequestHeader(key, headers[key]));
			}
			return ret;
		}
		
		private function cleanLoader(loader:URLLoader):void
		{
			loader.removeEventListener(Event.COMPLETE, handleComplete);
			loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleError);
			loader.removeEventListener(IOErrorEvent.IO_ERROR, handleError);
			delete loaders[loader];
		}
		
		// Request event handlers
		
		private function handleError(e:ErrorEvent):void
		{
			call('error', e.text);
			cleanLoader(e.target as URLLoader);
		}
		
		private function handleComplete(e:Event):void
		{
			var loader:URLLoader = e.target as URLLoader;
			var id:int = loaders[loader];
			call('success', id, escape(loader.data));
			cleanLoader(loader);
		}
		
		// JS Communication
		
		private function call(method:String, ...args):void
		{
			args.unshift('Cajax._' + method);
			ExternalInterface.call.apply(null, args);
		}
	}
}